/*
 * Copyright 2010-2016 JetBrains s.r.o.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.jetbrains.kotlin.idea.internal;

import com.intellij.ide.highlighter.JavaFileType;
import com.intellij.openapi.Disposable;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.editor.EditorFactory;
import com.intellij.openapi.editor.ScrollType;
import com.intellij.openapi.fileEditor.FileEditorManager;
import com.intellij.openapi.progress.ProcessCanceledException;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.Messages;
import com.intellij.openapi.util.Computable;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.wm.ToolWindow;
import com.intellij.util.Alarm;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.java.decompiler.IdeaLogger;
import org.jetbrains.kotlin.backend.common.output.OutputFile;
import org.jetbrains.kotlin.backend.common.output.OutputFileCollection;
import org.jetbrains.kotlin.backend.jvm.JvmIrCodegenFactory;
import org.jetbrains.kotlin.codegen.ClassBuilderFactories;
import org.jetbrains.kotlin.codegen.CompilationErrorHandler;
import org.jetbrains.kotlin.codegen.DefaultCodegenFactory;
import org.jetbrains.kotlin.codegen.KotlinCodegenFacade;
import org.jetbrains.kotlin.codegen.state.GenerationState;
import org.jetbrains.kotlin.config.*;
import org.jetbrains.kotlin.diagnostics.Diagnostic;
import org.jetbrains.kotlin.diagnostics.rendering.DefaultErrorMessages;
import org.jetbrains.kotlin.idea.caches.resolve.ResolutionUtils;
import org.jetbrains.kotlin.idea.debugger.DebuggerUtils;
import org.jetbrains.kotlin.idea.project.PlatformKt;
import org.jetbrains.kotlin.idea.resolve.ResolutionFacade;
import org.jetbrains.kotlin.idea.util.InfinitePeriodicalTask;
import org.jetbrains.kotlin.idea.util.LongRunningReadTask;
import org.jetbrains.kotlin.idea.util.ProjectRootsUtil;
import org.jetbrains.kotlin.psi.KtClassOrObject;
import org.jetbrains.kotlin.psi.KtFile;
import org.jetbrains.kotlin.psi.KtScript;
import org.jetbrains.kotlin.resolve.BindingContext;
import org.jetbrains.kotlin.utils.StringsKt;

import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.*;
import java.util.List;

public class KotlinBytecodeToolWindow extends JPanel implements Disposable {
    private final Logger LOG = Logger.getInstance(KotlinBytecodeToolWindow.class);

    private static final int UPDATE_DELAY = 1000;
    private static final String DEFAULT_TEXT = "/*\n" +
                                               "Generated bytecode for Kotlin source file.\n" +
                                               "No Kotlin source file is opened.\n" +
                                               "*/";

    private class UpdateBytecodeToolWindowTask extends LongRunningReadTask<Location, String> {
        @Override
        protected Location prepareRequestInfo() {
            if (!toolWindow.isVisible()) {
                return null;
            }

            Location location = Location.fromEditor(FileEditorManager.getInstance(myProject).getSelectedTextEditor(), myProject);
            if (location.getEditor() == null) {
                return null;
            }

            KtFile file = location.getKFile();
            if (file == null || !ProjectRootsUtil.isInProjectSource(file)) {
                return null;
            }

            return location;
        }

        @NotNull
        @Override
        protected Location cloneRequestInfo(@NotNull Location location) {
            Location newLocation = super.cloneRequestInfo(location);
            assert location.equals(newLocation) : "cloneRequestInfo should generate same location object";
            return newLocation;
        }

        @Override
        protected void hideResultOnInvalidLocation() {
            setText(DEFAULT_TEXT);
        }

        @NotNull
        @Override
        protected String processRequest(@NotNull Location location) {
            KtFile ktFile = location.getKFile();
            assert ktFile != null;

            CompilerConfiguration configuration = new CompilerConfiguration();
            if (!enableInline.isSelected()) {
                configuration.put(CommonConfigurationKeys.DISABLE_INLINE, true);
            }
            if (!enableAssertions.isSelected()) {
                configuration.put(JVMConfigurationKeys.DISABLE_CALL_ASSERTIONS, true);
                configuration.put(JVMConfigurationKeys.DISABLE_PARAM_ASSERTIONS, true);
            }
            if (!enableOptimization.isSelected()) {
                configuration.put(JVMConfigurationKeys.DISABLE_OPTIMIZATION, true);
            }

            if (jvm8Target.isSelected()) {
                configuration.put(JVMConfigurationKeys.JVM_TARGET, JvmTarget.JVM_1_8);
            }

            if (ir.isSelected()) {
                configuration.put(JVMConfigurationKeys.IR, true);
            }

            CommonConfigurationKeysKt.setLanguageVersionSettings(configuration, PlatformKt.getLanguageVersionSettings(ktFile));

            return getBytecodeForFile(ktFile, configuration);
        }

        @Override
        protected void onResultReady(@NotNull Location requestInfo, String resultText) {
            Editor editor = requestInfo.getEditor();
            assert editor != null;

            if (resultText == null) {
                return;
            }

            setText(resultText);

            int fileStartOffset = requestInfo.getStartOffset();
            int fileEndOffset = requestInfo.getEndOffset();

            Document document = editor.getDocument();
            int startLine = document.getLineNumber(fileStartOffset);
            int endLine = document.getLineNumber(fileEndOffset);
            if (endLine > startLine && fileEndOffset > 0 && document.getCharsSequence().charAt(fileEndOffset - 1) == '\n') {
                endLine--;
            }

            Document byteCodeDocument = myEditor.getDocument();

            Pair<Integer, Integer> linesRange = mapLines(byteCodeDocument.getText(), startLine, endLine);
            int endSelectionLineIndex = Math.min(linesRange.second + 1, byteCodeDocument.getLineCount());

            int startOffset = byteCodeDocument.getLineStartOffset(linesRange.first);
            int endOffset = Math.min(byteCodeDocument.getLineStartOffset(endSelectionLineIndex), byteCodeDocument.getTextLength());

            myEditor.getCaretModel().moveToOffset(endOffset);
            myEditor.getScrollingModel().scrollToCaret(ScrollType.MAKE_VISIBLE);
            myEditor.getCaretModel().moveToOffset(startOffset);
            myEditor.getScrollingModel().scrollToCaret(ScrollType.MAKE_VISIBLE);

            myEditor.getSelectionModel().setSelection(startOffset, endOffset);
        }
    }

    private final Editor myEditor;
    private final Project myProject;
    private final ToolWindow toolWindow;
    private final JCheckBox enableInline;
    private final JCheckBox enableOptimization;
    private final JCheckBox enableAssertions;
    private final JButton decompile;
    private final JCheckBox jvm8Target;
    private final JCheckBox ir;

    public KotlinBytecodeToolWindow(Project project, ToolWindow toolWindow) {
        super(new BorderLayout());
        myProject = project;
        this.toolWindow = toolWindow;

        myEditor = EditorFactory.getInstance().createEditor(
                EditorFactory.getInstance().createDocument(""), project, JavaFileType.INSTANCE, true);
        add(myEditor.getComponent());

        JPanel optionPanel = new JPanel(new FlowLayout());
        add(optionPanel, BorderLayout.NORTH);

        decompile = new JButton("Decompile");
        if (KotlinDecompilerService.Companion.getInstance() != null) {
            optionPanel.add(decompile);
            decompile.addActionListener(new ActionListener() {
                @Override
                public void actionPerformed(ActionEvent e) {
                    Location location = Location.fromEditor(FileEditorManager.getInstance(myProject).getSelectedTextEditor(), myProject);
                    KtFile file = location.getKFile();
                    if (file != null) {
                        try {
                            KotlinDecompilerAdapterKt.showDecompiledCode(file);
                        }
                        catch (IdeaLogger.InternalException ex) {
                            LOG.info(ex);
                            Messages.showErrorDialog(myProject, "Failed to decompile " + file.getName() + ": " + ex, "Kotlin Bytecode Decompiler");
                        }
                    }
                }
            });
        }

        /*TODO: try to extract default parameter from compiler options*/
        enableInline = new JCheckBox("Inline", true);
        enableOptimization = new JCheckBox("Optimization", true);
        enableAssertions = new JCheckBox("Assertions", true);
        jvm8Target = new JCheckBox("JVM 8 target", false);
        ir = new JCheckBox("IR", false);
        optionPanel.add(enableInline);
        optionPanel.add(enableOptimization);
        optionPanel.add(enableAssertions);
        optionPanel.add(ir);
        optionPanel.add(jvm8Target);

        new InfinitePeriodicalTask(UPDATE_DELAY, Alarm.ThreadToUse.SWING_THREAD, this, new Computable<LongRunningReadTask>() {
            @Override
            public LongRunningReadTask compute() {
                return new UpdateBytecodeToolWindowTask();
            }
        }).start();

        setText(DEFAULT_TEXT);
    }

    // public for tests
    @NotNull
    public static String getBytecodeForFile(@NotNull KtFile ktFile, @NotNull CompilerConfiguration configuration) {
        GenerationState state;
        try {
            state = compileSingleFile(ktFile, configuration);
        }
        catch (ProcessCanceledException e) {
            throw e;
        }
        catch (Exception e) {
            return printStackTraceToString(e);
        }

        StringBuilder answer = new StringBuilder();

        Collection<Diagnostic> diagnostics = state.getCollectedExtraJvmDiagnostics().all();
        if (!diagnostics.isEmpty()) {
            answer.append("// Backend Errors: \n");
            answer.append("// ================\n");
            for (Diagnostic diagnostic : diagnostics) {
                answer.append("// Error at ")
                        .append(diagnostic.getPsiFile().getName())
                        .append(StringsKt.join(diagnostic.getTextRanges(), ","))
                        .append(": ")
                        .append(DefaultErrorMessages.render(diagnostic))
                        .append("\n");
            }
            answer.append("// ================\n\n");
        }

        OutputFileCollection outputFiles = state.getFactory();
        for (OutputFile outputFile : outputFiles.asList()) {
            answer.append("// ================");
            answer.append(outputFile.getRelativePath());
            answer.append(" =================\n");
            answer.append(outputFile.asText()).append("\n\n");
        }

        return answer.toString();
    }

    @NotNull
    public static GenerationState compileSingleFile(
            @NotNull final KtFile ktFile,
            @NotNull CompilerConfiguration configuration
    ) {
        ResolutionFacade resolutionFacade = ResolutionUtils.getResolutionFacade(ktFile);

        BindingContext bindingContextForFile = resolutionFacade.analyzeFullyAndGetResult(Collections.singletonList(ktFile)).getBindingContext();

        kotlin.Pair<BindingContext, List<KtFile>> result = DebuggerUtils.INSTANCE.analyzeInlinedFunctions(
                resolutionFacade, ktFile, configuration.getBoolean(CommonConfigurationKeys.DISABLE_INLINE),
                bindingContextForFile
        );

        BindingContext bindingContext = result.getFirst();
        List<KtFile> toProcess = result.getSecond();

        GenerationState.GenerateClassFilter generateClassFilter = new GenerationState.GenerateClassFilter() {
            @Override
            public boolean shouldGeneratePackagePart(@NotNull KtFile file) {
                return file == ktFile;
            }

            @Override
            public boolean shouldAnnotateClass(@NotNull KtClassOrObject processingClassOrObject) {
                return true;
            }

            @Override
            public boolean shouldGenerateClass(@NotNull KtClassOrObject processingClassOrObject) {
                return processingClassOrObject.getContainingKtFile() == ktFile;
            }

            @Override
            public boolean shouldGenerateScript(@NotNull KtScript script) {
                return script.getContainingKtFile() == ktFile;
            }
        };

        GenerationState state = new GenerationState(
                ktFile.getProject(), ClassBuilderFactories.TEST, resolutionFacade.getModuleDescriptor(), bindingContext, toProcess,
                configuration, generateClassFilter,
                configuration.getBoolean(JVMConfigurationKeys.IR) ? JvmIrCodegenFactory.INSTANCE : DefaultCodegenFactory.INSTANCE
        );

        KotlinCodegenFacade.compileCorrectFiles(state, CompilationErrorHandler.THROW_EXCEPTION);

        return state;
    }

    private static Pair<Integer, Integer> mapLines(String text, int startLine, int endLine) {
        int byteCodeLine = 0;
        int byteCodeStartLine = -1;
        int byteCodeEndLine = -1;

        List<Integer> lines = new ArrayList<Integer>();
        for (String line : text.split("\n")) {
            line = line.trim();

            if (line.startsWith("LINENUMBER")) {
                int ktLineNum = new Scanner(line.substring("LINENUMBER".length())).nextInt() - 1;
                lines.add(ktLineNum);
            }
        }
        Collections.sort(lines);

        for (Integer line : lines) {
            if (line >= startLine) {
                startLine = line;
                break;
            }
        }

        for (String line : text.split("\n")) {
            line = line.trim();

            if (line.startsWith("LINENUMBER")) {
                int ktLineNum = new Scanner(line.substring("LINENUMBER".length())).nextInt() - 1;

                if (byteCodeStartLine < 0 && ktLineNum == startLine) {
                    byteCodeStartLine = byteCodeLine;
                }

                if (byteCodeStartLine > 0 && ktLineNum > endLine) {
                    byteCodeEndLine = byteCodeLine - 1;
                    break;
                }
            }

            if (byteCodeStartLine >= 0 && (line.startsWith("MAXSTACK") || line.startsWith("LOCALVARIABLE") || line.isEmpty())) {
                byteCodeEndLine = byteCodeLine - 1;
                break;
            }


            byteCodeLine++;
        }

        if (byteCodeStartLine == -1 || byteCodeEndLine == -1) {
            return new Pair<Integer, Integer>(0, 0);
        }
        else {
            return new Pair<Integer, Integer>(byteCodeStartLine, byteCodeEndLine);
        }
    }

    private static String printStackTraceToString(Throwable e) {
        StringWriter out = new StringWriter(1024);
        try (PrintWriter printWriter = new PrintWriter(out)) {
            e.printStackTrace(printWriter);
            return out.toString().replace("\r", "");
        }
    }

    private void setText(@NotNull final String resultText) {
        ApplicationManager.getApplication().runWriteAction(new Runnable() {
            @Override
            public void run() {
                myEditor.getDocument().setText(StringUtil.convertLineSeparators(resultText));
            }
        });
    }

    @Override
    public void dispose() {
        EditorFactory.getInstance().releaseEditor(myEditor);
    }
}
